feat(slack): native OAuth trigger#5323
Conversation
Add assistant:write, app_mentions:read, and im:history to the Slack bot OAuth scopes so the Set Assistant Status / Title / Suggested Prompts tools (assistant.threads.*) work with users' existing Slack credentials — no new app or credentials required. Restore the action_assistant trigger capability (scope assistant:write) in the manifest generator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpeT8J5yVCrrNQB9Hzm9uS
…e, add channel-id input
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview Trigger model is redesigned around one event per block ( Editor changes treat Reviewed by Cursor Bugbot for commit 4692708. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryRedesigns the Sim native Slack trigger from a multi-select "operations" model to a single-event-per-block model backed by a unified event catalog (
Confidence Score: 4/5Safe to merge for all event types except The apps/sim/lib/webhooks/providers/slack.ts — the Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Slack
participant Ingest as /api/webhooks/slack
participant DB as Database
participant Filter as shouldSkipSlackTriggerEvent
participant Queue as queueWebhookExecution
Slack->>Ingest: POST (HMAC-signed event)
Ingest->>Ingest: verifySlackRequestSignature
Ingest->>Ingest: extract team_id + authorizations[].team_id
Ingest->>DB: findWebhooksByRoutingKey(team_id) per workspace
DB-->>Ingest: matching slack_app webhooks (deduplicated)
loop for each webhook
Ingest->>Filter: shouldSkipSlackTriggerEvent(payload, providerConfig)
Filter-->>Ingest: "skip=true/false"
alt "skip=false"
Ingest->>DB: blockExistsInDeployment check
Ingest->>Queue: queueWebhookExecution
end
end
Ingest-->>Slack: HTTP 200
note over DB,Queue: formatInput (later, async)
Queue->>DB: resolve credentialId to account owner
Queue->>Slack: auth.test / reactions.get / files download (if needed)
Queue-->>Queue: SlackTriggerEvent output
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Slack
participant Ingest as /api/webhooks/slack
participant DB as Database
participant Filter as shouldSkipSlackTriggerEvent
participant Queue as queueWebhookExecution
Slack->>Ingest: POST (HMAC-signed event)
Ingest->>Ingest: verifySlackRequestSignature
Ingest->>Ingest: extract team_id + authorizations[].team_id
Ingest->>DB: findWebhooksByRoutingKey(team_id) per workspace
DB-->>Ingest: matching slack_app webhooks (deduplicated)
loop for each webhook
Ingest->>Filter: shouldSkipSlackTriggerEvent(payload, providerConfig)
Filter-->>Ingest: "skip=true/false"
alt "skip=false"
Ingest->>DB: blockExistsInDeployment check
Ingest->>Queue: queueWebhookExecution
end
end
Ingest-->>Slack: HTTP 200
note over DB,Queue: formatInput (later, async)
Queue->>DB: resolve credentialId to account owner
Queue->>Slack: auth.test / reactions.get / files download (if needed)
Queue-->>Queue: SlackTriggerEvent output
Reviews (7): Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." | Re-trigger Greptile |
# Conflicts: # apps/sim/blocks/blocks/slack.ts # apps/sim/lib/webhooks/deploy.ts # packages/db/migrations/meta/0253_snapshot.json # packages/db/migrations/meta/_journal.json # scripts/check-api-validation-contracts.ts
|
@greptile review |
…oken for reaction text + file downloads
|
@greptile review — addressed the three findings: (1) unmapped/unselected events now drop instead of bypassing the Operations filter, (2) formatInput resolves the OAuth bot token for slack_app webhooks so reaction-message text works, (3) re-added the includeFiles toggle + token resolution so native file downloads work. |
…token via credential owner not execution actor
|
@greptile review — addressed both: empty operation selection now fires nothing (no match-all), and the OAuth token is resolved via the credential owner (not the execution actor) so it works in shared workspaces. |
…r channel picker over stale manual ids
…ull event catalog
|
@greptile review |
| error: { message: 'Select a Slack account for the trigger.', status: 400 }, | ||
| } | ||
| } | ||
| const botToken = await refreshAccessTokenIfNeeded(credentialId, userId, requestId) |
There was a problem hiding this comment.
Deploy uses deployer not owner
High Severity
Sim-mode Slack deploy resolves team_id via refreshAccessTokenIfNeeded using the deploying user’s id, but that helper only loads OAuth tokens when the account row belongs to that user. In shared workspaces, a teammate deploying a trigger wired to someone else’s Slack credential gets a failed deploy even though runtime formatInput already resolves the credential owner for the same case.
Reviewed by Cursor Bugbot for commit 65c1415. Configure here.
|
Fixed: custom (bring-your-own-app) Slack deliveries now apply the same event/source/threads/emoji/channel/bot filtering as the native app. Extracted the filter into a shared @greptile review |
# Conflicts: # apps/sim/lib/webhooks/deploy.ts # apps/sim/lib/webhooks/utils.server.ts # packages/db/migrations/meta/0254_snapshot.json # packages/db/migrations/meta/_journal.json # packages/db/schema.ts # scripts/check-api-validation-contracts.ts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4692708. Configure here.
| effectiveProvider = 'slack_app' | ||
| effectivePath = null | ||
| } else { | ||
| effectiveProvider = 'slack' |
There was a problem hiding this comment.
Custom reactions lack self-drop
Medium Severity
For slack_oauth custom app deploys, bot_user_id is never written to providerConfig, while Sim mode sets it via auth.test. Shared filtering treats reaction self-drop only when bot_user_id matches event.user, so a custom reaction trigger can fire on the bot’s own reactions and risk runaway workflows.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 4692708. Configure here.
|
Rebased onto staging (resolved the @greptile review |
| export function resolveSlackEventChannel( | ||
| event: Record<string, unknown> | undefined | ||
| ): string | undefined { | ||
| if (!event) return undefined | ||
| if (typeof event.channel === 'string') return event.channel | ||
| const item = event.item as Record<string, unknown> | undefined | ||
| if (typeof item?.channel === 'string') return item.channel | ||
| return typeof event.channel_id === 'string' ? event.channel_id : undefined | ||
| } |
There was a problem hiding this comment.
For
channel_rename events, Slack sends channel as an object {"id":"C024BE91L","name":"newname",...} rather than a bare string. resolveSlackEventChannel only checks typeof event.channel === 'string', so it returns undefined for every rename payload. Because channel_rename is marked filters: ['channels'] in the catalog, channelScoped evaluates to true whenever a channel filter is configured. The guard !eventChannel || !selectedChannels.includes(eventChannel) then fires on every channel_rename event, silently dropping all of them regardless of which channel the user selected. Adding an object branch before the item.channel check fixes it with no impact on other event types.
| export function resolveSlackEventChannel( | |
| event: Record<string, unknown> | undefined | |
| ): string | undefined { | |
| if (!event) return undefined | |
| if (typeof event.channel === 'string') return event.channel | |
| const item = event.item as Record<string, unknown> | undefined | |
| if (typeof item?.channel === 'string') return item.channel | |
| return typeof event.channel_id === 'string' ? event.channel_id : undefined | |
| } | |
| export function resolveSlackEventChannel( | |
| event: Record<string, unknown> | undefined | |
| ): string | undefined { | |
| if (!event) return undefined | |
| if (typeof event.channel === 'string') return event.channel | |
| // channel_rename sends channel as an object {id, name, ...} — extract the id. | |
| const channelObj = event.channel as Record<string, unknown> | undefined | |
| if (channelObj && typeof channelObj.id === 'string') return channelObj.id | |
| const item = event.item as Record<string, unknown> | undefined | |
| if (typeof item?.channel === 'string') return item.channel | |
| return typeof event.channel_id === 'string' ? event.channel_id : undefined | |
| } |


Summary
Redesigns the native Slack trigger around a single event per block with contextual filters, replacing the multi-select "Operations" model.
eventType); compose complex behavior with multiple blocks instead of a downstream condition blockSLACK_EVENT_CATALOG) as the single source of truth — message, app mentioned, reactions, message edited/deleted, file shared, member/channel lifecycle, pins, workspace join, app home, assistant — driving UI gating and route filteringcapabilities.ts). Deploy rejects an unsupported event on Sim modeauthorizations[]; message edit/delete subtypes mapped; DMs bypass the channel filter;pins:readaddedtrigger-advancedfields (generalizes the block-level advanced section; zero blast radius — every existing such field is canonical-paired)Type of Change
Testing
route.test.ts): source/threads/emoji/channel gating, self-drop, subtype mapping, legacy back-compat, DM-bypasses-channel-filter — all passingbunx tsc --noEmitclean,bun run lint:checkclean,bun run check:api-validation:strictpassedChecklist